﻿using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using Win104.Tools;

namespace Win104.ZZH
{
    public class TcpSocketClient : IDisposable
    {
        #region Fields

        private TcpClient _tcpClient;
        private readonly TcpSocketClientConfiguration _configuration;
        private readonly IPEndPoint _remoteEndPoint;
        private readonly IPEndPoint _localEndPoint;
        private Stream _stream;
        private byte[] _receiveBuffer;
        private int _receiveBufferOffset = 0;

        private int _state;
        private const int _none = 0;
        private const int _connecting = 1;
        private const int _connected = 2;
        private const int _closed = 5;

        #endregion

        #region Constructors

        public TcpSocketClient(IPEndPoint remoteEP, TcpSocketClientConfiguration configuration = null)
            : this(remoteEP, null, configuration)
        {
        }

        public TcpSocketClient(IPEndPoint remoteEP, IPEndPoint localEP, TcpSocketClientConfiguration configuration = null)
        {
            if (remoteEP == null)
                throw new ArgumentNullException("remoteEP");

            _remoteEndPoint = remoteEP;
            _localEndPoint = localEP;
            _configuration = configuration ?? new TcpSocketClientConfiguration();

        }

        #endregion

        #region Properties

        public TimeSpan ConnectTimeout { get { return _configuration.ConnectTimeout; } }

        private bool Connected { get { return _tcpClient != null && _tcpClient.Client.Connected; } }
        public IPEndPoint RemoteEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.RemoteEndPoint : _remoteEndPoint; } }
        public IPEndPoint LocalEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.LocalEndPoint : _localEndPoint; } }

        public SocketConnectionState State
        {
            get
            {
                switch (_state)
                {
                    case _none:
                        return SocketConnectionState.None;
                    case _connecting:
                        return SocketConnectionState.Connecting;
                    case _connected:
                        return SocketConnectionState.Connected;
                    case _closed:
                        return SocketConnectionState.Closed;
                    default:
                        return SocketConnectionState.Closed;
                }
            }
        }

        public override string ToString()
        {
            return string.Format("RemoteEndPoint[{0}], LocalEndPoint[{1}]",
                this.RemoteEndPoint, this.LocalEndPoint);
        }

        #endregion

        #region Connect

        public void Connect()
        {
            //以原子操作的形式，将变量设置为指定值
            int origin = Interlocked.Exchange(ref _state, _connecting);
            if (!(origin == _none || origin == _closed))
            {
                Close(false); 
                throw new InvalidOperationException("This tcp socket client is in invalid state when connecting.");
            }

            Clean(); // 强制清理

            _tcpClient = _localEndPoint != null ?
                new TcpClient(_localEndPoint) :
                new TcpClient(_remoteEndPoint.Address.AddressFamily);
            SetSocketOptions();

            _receiveBuffer = new byte[_configuration.ReceiveBufferSize];
            _receiveBufferOffset = 0;

            var ar = _tcpClient.BeginConnect(_remoteEndPoint.Address, _remoteEndPoint.Port, null, _tcpClient);
            if (!ar.AsyncWaitHandle.WaitOne(ConnectTimeout))
            {
                Close(false); // 连接超时
                throw new TimeoutException(string.Format(
                    "Connect to [{0}] timeout [{1}].", _remoteEndPoint, ConnectTimeout));
            }
            _tcpClient.EndConnect(ar);

            if (Interlocked.CompareExchange(ref _state, _connected, _connecting) != _connecting)
            {
                Close(false); // 错误的链接状态
                throw new InvalidOperationException("This tcp socket client is in invalid state when connected.");
            }

            HandleTcpServerConnected();
        }

        public void Close()
        {
            Close(true);
        }

        private void Close(bool shallNotifyUserSide)
        {
            if (Interlocked.Exchange(ref _state, _closed) == _closed)
            {
                return;
            }

            Shutdown();

            if (shallNotifyUserSide)
            {
                try
                {
                    RaiseServerDisconnected();
                }
                catch (Exception ex) // catch all exceptions from out-side
                {
                    HandleUserSideError(ex);
                }
            }

            Clean();
        }

        public void Shutdown()
        {
            //在全双工会话中，正确的方法是调用socket.Shutdown，并且给远端一些时间关闭他们的发送通道
            //这样能确保能收到挂起的数据
            if (_tcpClient != null && _tcpClient.Connected)
            {
                _tcpClient.Client.Shutdown(SocketShutdown.Send);
            }
        }

        private void Clean()
        {
            try
            {
                try
                {
                    if (_stream != null)
                    {
                        _stream.Dispose();
                    }
                }
                catch { }
                try
                {
                    if (_tcpClient != null)
                    {
                        _tcpClient.Close();
                    }
                }
                catch { }
            }
            catch { }
            finally
            {
                _stream = null;
                _tcpClient = null;
            }

            _receiveBufferOffset = 0;
        }

        #endregion

        #region Receive

        private void HandleTcpServerConnected()
        {
            try
            {
                _stream = _tcpClient.GetStream();

                bool isErrorOccurredInUserSide = false;
                try
                {
                    RaiseServerConnected();
                }
                catch (Exception ex) // catch all exceptions from out-side
                {
                    isErrorOccurredInUserSide = true;
                    HandleUserSideError(ex);
                }

                if (!isErrorOccurredInUserSide)
                {
                    ContinueReadBuffer();
                }
                else
                {
                    Close(true); // user side handle tcp connection error occurred
                }
            }
            catch (TimeoutException ex)
            {
                WriteLog.WriteLogs(ex.Message);
                Close(false); // 超时异常
                throw;
            }
            catch (Exception ex) // 捕获异常并记录，然后再抛出
            {
                WriteLog.WriteLogs(ex.Message);
                Close(true); // handle tcp connection error occurred
                throw;
            }
        }

        private void SetSocketOptions()
        {
            _tcpClient.ReceiveBufferSize = _configuration.ReceiveBufferSize;
            _tcpClient.SendBufferSize = _configuration.SendBufferSize;
            _tcpClient.ReceiveTimeout = (int)_configuration.ReceiveTimeout.TotalMilliseconds;
            _tcpClient.SendTimeout = (int)_configuration.SendTimeout.TotalMilliseconds;
            _tcpClient.NoDelay = _configuration.NoDelay;
            _tcpClient.LingerState = _configuration.LingerState;

            if (_configuration.KeepAlive)
            {
                _tcpClient.Client.SetSocketOption(
                    SocketOptionLevel.Socket,
                    SocketOptionName.KeepAlive,
                    (int)_configuration.KeepAliveInterval.TotalMilliseconds);
            }

            _tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, _configuration.ReuseAddress);
        }

        private void ContinueReadBuffer()
        {
            try
            {
                _stream.BeginRead(
                    _receiveBuffer,
                    _receiveBufferOffset,
                    _receiveBuffer.Length - _receiveBufferOffset,
                    HandleDataReceived,
                    _stream);
            }
            catch (Exception ex) 
            {
                HandleReceiveOperationException(ex);
                throw;
            }
        }

        private void HandleDataReceived(IAsyncResult ar)
        {
            if (this.State != SocketConnectionState.Connected
                || _stream == null)
            {
                Close(false); // receive buffer callback
                return;
            }

            try
            {
                int numberOfReadBytes = 0;
                try
                {
                    //EndRead对应异步的BeginRead，在收到数据之前EndRead是阻塞的
                    //EndRead最多读取BeginRead中缓冲区大小的数据
                    //如果远程主机强制关闭链接，EndRead方法会立即完成并返回0字节 
                    numberOfReadBytes = _stream.EndRead(ar);
                }
                catch (Exception)
                {
                    //无法从链接读取数据，现有链接被远程主机强制关闭 
                    numberOfReadBytes = 0;
                }

                if (numberOfReadBytes == 0)
                {
                    Close(true); //收到0个字节意味着链接已关闭 
                    return;
                }

                ReceiveBuffer(numberOfReadBytes);

                ContinueReadBuffer();
            }
            catch (Exception ex)
            {
                try
                {
                    HandleReceiveOperationException(ex);
                }
                catch (Exception innnerException)
                {
                    WriteLog.WriteLogs(innnerException.Message);
                }
            }
        }

        private void ReceiveBuffer(int receiveCount)
        {
            //TCP保证以正确的顺序传递数据包，但是不能保证发送方的一次发送能引起接收方一次接收
            //它可能会引起接收方的多次接收
            //这就可能导致没有完全接收发送的数据
            //一般的做法是把收到的数据放入缓冲区，然后将缓冲区的字节流拆分解析
            //1.使用固定长度的消息，每条消息都是固定长度
            //2.使用指定长度的消息头来记录正文长度，消息头的前几个字节记录消息正文的长度
            //3.使用分隔符，比如换行符等
           
            while (true)
            {
                if (isFinish(_receiveBuffer, _receiveBufferOffset, receiveCount))
                {
                    try
                    {
                        RaiseServerDataReceived(_receiveBuffer, 0, _receiveBufferOffset+receiveCount);
                    }
                    catch (Exception ex) // catch all exceptions from out-side
                    {
                        HandleUserSideError(ex);
                    }
                    finally
                    {
                        receiveCount = 0;
                        _receiveBufferOffset = 0;
                    }
                }
                else
                {
                    break;
                }
            }

            _receiveBufferOffset += receiveCount;

        }

        private bool isFinish(byte[] payload, int payloadOffset, int payloadCount)
        {
            //这里可以加上接收完成的业务逻辑，比如分隔符
            if (payloadCount < 2)
                return false;
            else
                return true;
        }

        #endregion

        #region Exception Handler

        private void HandleSendOperationException(Exception ex)
        {
            if (IsSocketTimeOut(ex))
            {
                CloseIfShould(ex);
                throw new Exception(ex.Message, new TimeoutException(ex.Message, ex));
            }

            CloseIfShould(ex);
            throw new Exception(ex.Message, ex);
        }

        private void HandleReceiveOperationException(Exception ex)
        {
            if (IsSocketTimeOut(ex))
            {
                CloseIfShould(ex);
                throw new Exception(ex.Message, new TimeoutException(ex.Message, ex));
            }

            CloseIfShould(ex);
            throw new Exception(ex.Message, ex);
        }

        private bool IsSocketTimeOut(Exception ex)
        {
            return ex is IOException
                && ex.InnerException != null
                && ex.InnerException is SocketException
                && (ex.InnerException as SocketException).SocketErrorCode == SocketError.TimedOut;
        }

        private bool CloseIfShould(Exception ex)
        {
            if (ex is ObjectDisposedException
                || ex is InvalidOperationException
                || ex is SocketException
                || ex is IOException
                || ex is NullReferenceException // buffer array operation
                || ex is ArgumentException      // buffer array operation
                )
            {
                WriteLog.WriteLogs(ex.Message);

                Close(true); // catch specified exception then intend to close the session

                return true;
            }

            return false;
        }

        private void HandleUserSideError(Exception ex)
        {
            WriteLog.WriteLogs(string.Format("Client [{0}] error occurred in user side [{1}].", this, ex.Message));
        }

        #endregion

        #region Send

        public void Send(byte[] data)
        {
            if (data == null)
                throw new ArgumentNullException("data");

            Send(data, 0, data.Length);
        }

        public void Send(byte[] data, int offset, int count)
        {
            if (this.State != SocketConnectionState.Connected)
            {
                throw new InvalidProgramException("This client has not connected to server.");
            }

            try
            {
                _stream.Write(data, offset, count);
            }
            catch (Exception ex) // catch exceptions then log then re-throw
            {
                HandleSendOperationException(ex);
                throw;
            }
        }

        #endregion

        #region Events

        public event EventHandler<TcpServerConnectedEventArgs> ServerConnected;
        public event EventHandler<TcpServerDisconnectedEventArgs> ServerDisconnected;
        public event EventHandler<TcpServerDataReceivedEventArgs> ServerDataReceived;

        private void RaiseServerConnected()
        {
            if (ServerConnected != null)
            {
                ServerConnected(this, new TcpServerConnectedEventArgs(this.RemoteEndPoint, this.LocalEndPoint));
            }
        }

        private void RaiseServerDisconnected()
        {
            if (ServerDisconnected != null)
            {
                ServerDisconnected(this, new TcpServerDisconnectedEventArgs(_remoteEndPoint, _localEndPoint));
            }
        }

        private void RaiseServerDataReceived(byte[] data, int dataOffset, int dataLength)
        {
            if (ServerDataReceived != null)
            {
                ServerDataReceived(this, new TcpServerDataReceivedEventArgs(this, data, dataOffset, dataLength));
            }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                try
                {
                    Close(true); // disposing
                }
                catch (Exception ex)
                {
                    WriteLog.WriteLogs(ex.Message);
                }
            }
        }

        #endregion
    }
}
